# -*- coding:utf-8 -*-
"""
日志记录模块
需要修改自身的logging模块以实现多进程之间安全的记录
"""
__author__ = "sunsn"

import os
import time
import yaml

try:
    import fcntl
except ImportError:
    import utils.fcntl as fcntl
import logging
from logging.handlers import TimedRotatingFileHandler
from PyTimedTasks.settings import LOG_LEVEL, LOG_FORMATTER, LOG_DATE_FORMATTER, LOG_FILE4PROJ, LOG_FILE4TASKS


class SafeTimedRotatingFileHandler(TimedRotatingFileHandler):
    """
    logging原生的TimedRotatingFileHandler不支持多进程操作
    本类重写doRollover方法，对日志加锁，保证文件的切换只有一个进程在做
    不过fcntl只在类unix系统中存在（是一个系统的c库）
    """

    def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False,
                 atTime=None, backup_dir=None):
        """
        新增加一个参数backup_dir。可以指定将滚动的日志保存到指定的目录
        """
        super(SafeTimedRotatingFileHandler, self).__init__(filename, when, interval, backupCount, encoding, delay, utc,
                                                           atTime)
        # 备份的日志放到其他文件夹
        self.backup_dir = backup_dir

    def getFilesToDelete(self):
        """
        Determine the files to delete when rolling over.

        More specific than the earlier method, which just used glob.glob().
        """
        dirName, baseName = os.path.split(self.baseFilename)

        # 当指定将备份的文件夹转到其他地方时，注意更换目录
        if self.backup_dir:
            dirName = self.backup_dir

        fileNames = os.listdir(dirName)
        result = []
        prefix = baseName + "."
        plen = len(prefix)
        for fileName in fileNames:
            if fileName[:plen] == prefix:
                suffix = fileName[plen:]
                if self.extMatch.match(suffix):
                    result.append(os.path.join(dirName, fileName))
        result.sort()
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def doRollover(self):
        """
        do a rollover; in this case, a date/time stamp is appended to the filename
        when the rollover happens.  However, you want the file to be named for the
        start of the interval, not the current time.  If there is a backup count,
        then we have to get a list of matching filenames, sort them and remove
        the one with the oldest suffix.
        """
        if self.stream:
            self.stream.close()
            self.stream = None
        # get the time that this sequence started at and make it a TimeTuple
        currentTime = int(time.time())
        dstNow = time.localtime(currentTime)[-1]
        t = self.rolloverAt - self.interval
        if self.utc:
            timeTuple = time.gmtime(t)
        else:
            timeTuple = time.localtime(t)
            dstThen = timeTuple[-1]
            if dstNow != dstThen:
                if dstNow:
                    addend = 3600
                else:
                    addend = -3600
                timeTuple = time.localtime(t + addend)

        # 滚动后的日志名称（加了日期）
        dfn = self.rotation_filename(self.baseFilename + "." +
                                     time.strftime(self.suffix, timeTuple))

        # 将dfn(滚动后的日志)切换到其他目录
        if self.backup_dir is not None:
            dfn_basename = os.path.basename(dfn)
            dfn = os.path.join(self.backup_dir, dfn_basename)

        # =====添加文件锁，保证多进程安全=====
        # if os.path.exists(dfn):
        #     os.remove(dfn)
        # self.rotate(self.baseFilename, dfn)
        if not os.path.exists(dfn) and os.path.exists(self.baseFilename):
            with open(self.baseFilename) as f:
                fcntl.flock(f, fcntl.LOCK_EX)
                self.rotate(self.baseFilename, dfn)
                # fcntl.flock(f, fcntl.LOCK_UN)
        # 对于文件的 close() 操作会使文件锁失效,不用刻意使用fcntl.LOCK_UN解锁
        # ========仅对类UNIX系统有效=========

        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                os.remove(s)
        if not self.delay:
            self.stream = self._open()
        newRolloverAt = self.computeRollover(currentTime)
        while newRolloverAt <= currentTime:
            newRolloverAt = newRolloverAt + self.interval
        # If DST changes and midnight or weekly rollover, adjust for this.
        if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
            dstAtRollover = time.localtime(newRolloverAt)[-1]
            if dstNow != dstAtRollover:
                if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
                    addend = -3600
                else:  # DST bows out before next rollover, so we need to add an hour
                    addend = 3600
                newRolloverAt += addend
        self.rolloverAt = newRolloverAt


class ProjectLogging(object):
    level_dict = {'critical': logging.CRITICAL, 'error': logging.ERROR,
                  'warning': logging.WARNING, 'info': logging.INFO,
                  'debug': logging.DEBUG, 'notset': logging.NOTSET}

    def __init__(self, level=None, formatter=None, datefmt=None, backup_dir=None, when='MIDNIGHT', interval=1,
                 backupCount=14, atTime=None):
        """
        '''
        初始化函数

        :param level: 表示日志的输出级别，默认info。支持logging定义的6中模式，info,debug等，只需传入对应的小写字符串
        :type level: `str`
        :param formatter: 表示日志的输出格式,默认使用配置文件的设置
        :type formatter: `str`
        :param datefmt: 表示日期的日志的输出格式
        :type datefmt: `str`
        :param backup_dir: 滚动的日志需要保存的目录
        :type backup_dir: `str`
        :param when: 滚动一次的单位,默认'MIDNIGHT'表示天，‘h'表示小时……
        :type when: `str`
        :param interval: 多久滚动一次，和when配合使用，如果when是MIDNIGHT,此是1，表示1天滚动一次日志
        :type interval: `int`
        :param backupCount: 表示保留多少个日志备份
        :type backupCount: `int`
        """
        try:
            self.level = self.level_dict[level] if level else logging.INFO
        except KeyError:
            self.level = logging.INFO

        self.formatter = formatter if formatter else "%(asctime)s - %(filename)s[line:%(lineno)d] - %(name)s[%(levelname)s]: %(message)s"

        self.backup_dir = backup_dir
        self.when = when
        self.interval = interval
        self.backupCount = backupCount
        self.datefmt = datefmt
        self.atTime = atTime

    def set_loglevel(self, level):
        '''
        可以在类外，通过实例化的对象，设置log level
        :param level: 日志的输出级别,支持logging定义的6中模式，info,debug等，只需传入对应的小写字符串
        :type level: `str`
        :return:
            无返回值
        '''
        try:
            self.level = self.level_dict[level] if level else logging.INFO
        except KeyError:
            self.level = logging.INFO

    def set_handler_formatter(self, formatter, datefmt=None):
        '''
        可以在类外，通过实例化的对象，设置formmater
        :param formatter: 表示日志的输出格式
        :type formatter: `str`
        :return:
        '''
        self.formatter = formatter
        self.datefmt = datefmt or "%Y-%m-%d %H:%M:%S"

    def set_rotating_params(self, when, interval, backupCount, atTime=None, backup_dir=None):
        self.when = when
        self.interval = interval
        self.backupCount = backupCount
        if atTime:
            self.atTime = atTime
        if backup_dir:
            self.backup_dir = backup_dir

    def get_logger(self, filename=None, logname=None, backup_dir=None, when=None, interval=None, backupCount=None,
                   atTime=None):
        '''
        获取日志的句柄，用来写日志
        :param filename: 日志文件的名称,日志文件的名称（带路径，否则会在本程序的运行路径下）
        :type filename: `str`
        :return:
            返回一个句柄
        '''
        filename = filename or "default.log"
        backup_dir = backup_dir or self.backup_dir
        when = when or self.when
        interval = interval or self.interval
        backupCount = backupCount or self.backupCount
        atTime = atTime or self.atTime

        # 获取handler
        safeTimedRotatingFileHandler = SafeTimedRotatingFileHandler(
            filename=filename,
            backup_dir=backup_dir,
            when=when,
            interval=interval,
            backupCount=backupCount,
            encoding='utf-8',
            atTime=atTime,
        )

        if self.datefmt is None:
            formatter = logging.Formatter(self.formatter)
        else:
            formatter = logging.Formatter(self.formatter, datefmt=self.datefmt)
        safeTimedRotatingFileHandler.setFormatter(formatter)
        # 可以分别给Handler设置日志级别
        # safeTimedRotatingFileHandler.setLevel(self.level)

        # 获取logger
        if logname is None:
            logger = logging.getLogger()
        else:
            logger = logging.getLogger(logname)

        # 为logger添加handler
        logger.addHandler(safeTimedRotatingFileHandler)
        # 设置logger的日志输出级别
        logger.setLevel(self.level)

        # 判断使用的是否是默认的日志文件，是的话，需要先在日志输出
        if filename == "default.log":
            logger.info("没有传入日志输出文件，将在本程序的运行目录下创建default.log文件输出")

        return logger


class CreateLoggerInstance(object):
    @classmethod
    def get_logger_instance(cls, logger_flag: str):
        if logger_flag == 'proj':
            # 1.实例化全局的日志对象,记录正常的日志
            # obj_logger4pro = ProjectLogging(LOG_LEVEL, LOG_FORMATTER)
            obj_logger4proj = ProjectLogging()
            obj_logger4proj.set_loglevel(level=LOG_LEVEL)
            obj_logger4proj.set_handler_formatter(formatter=LOG_FORMATTER,
                                                  datefmt=LOG_DATE_FORMATTER)
            logger = obj_logger4proj.get_logger(filename=LOG_FILE4PROJ, logname=logger_flag)
            return logger
        elif logger_flag == 'tasks':
            # 1.实例化全局的日志对象,记录正常的日志
            # obj_logger4pro = ProjectLogging(LOG_LEVEL, LOG_FORMATTER)
            obj_logger4proj = ProjectLogging()
            obj_logger4proj.set_loglevel(level=LOG_LEVEL)
            obj_logger4proj.set_handler_formatter(formatter=LOG_FORMATTER,
                                                  datefmt=LOG_DATE_FORMATTER)
            logger = obj_logger4proj.get_logger(filename=LOG_FILE4TASKS, logname=logger_flag)
            return logger
